﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Globalization;

namespace Sencha.Serialization
{
	public abstract class JsonWriterBase : IJsonWriter
	{
		[Flags]
		private enum Structure : byte { None = 0, IsContainer = 0x1, IsObject = 0x2 | IsContainer, IsArray = 0x4 | IsContainer, IsBegining = 0x1 << 7, IsBeginingOfContainer = IsContainer | IsBegining }

		private const long JsNumberMaxValueInt64 = 9007199254740992L;
		private const ulong JsNumberMaxValueUInt64 = 9007199254740992UL;
		private const double JsNumberMaxValueDouble = 9007199254740992.0;
		private const double JsNumberMaxValueSingle = 9007199254740992.0f;
		private const decimal JsNumberMaxValueDecimal = 9007199254740992.0m;

		private static readonly char[] Tabs = new char[] { '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t' };
		private static readonly char[] Newline = "\r\n".ToCharArray();
		private static readonly char[] NameSeparator = ":".ToCharArray();
		private static readonly char[] ValueSeparator = ",".ToCharArray();
		private static readonly char[] ArrayBegin = "[".ToCharArray();
		private static readonly char[] ArrayEnd = "]".ToCharArray();
		private static readonly char[] ObjectBegin = "{".ToCharArray();
		private static readonly char[] ObjectEnd = "}".ToCharArray();
		private static readonly char[] Quote = "\"".ToCharArray();
		private static readonly char[] Null = "null".ToCharArray();
		private static readonly char[] True = "true".ToCharArray();
		private static readonly char[] False = "false".ToCharArray();

		private Stack<Structure> m_struct = new Stack<Structure>(10);
		private long m_charsWriten = 0;
		private char[] m_outputBuffer = new char[512];
		private char[] m_tmpBuffer = new char[256];

		public ISerializationContext Context { get; private set; }
		public int InitialPadding { get; set; }
		public long CharactersWritten { get { return m_charsWriten; } protected set { m_charsWriten = value; } }

		protected JsonWriterBase(ISerializationContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			this.Context = context;
		}

		public abstract void Flush();
		public abstract void WriteJson(string jsonString);
		public abstract void WriteJson(char[] jsonString, int offset, int charactersToWrite);

		public void Write(JsonToken token)
		{
			this.WriteFormatting(token);

			switch (token)
			{
				case JsonToken.BeginArray:
					m_struct.Push(Structure.IsArray | Structure.IsBegining);
					this.WriteJson(ArrayBegin, 0, ArrayBegin.Length);
					break;
				case JsonToken.EndOfArray:
					m_struct.Pop();
					this.WriteJson(ArrayEnd, 0, ArrayEnd.Length);
					break;
				case JsonToken.BeginObject:
					m_struct.Push(Structure.IsObject | Structure.IsBegining);
					this.WriteJson(ObjectBegin, 0, ObjectBegin.Length);
					break;
				case JsonToken.EndOfObject:
					m_struct.Pop();
					this.WriteNewlineAndPad(0);
					this.WriteJson(ObjectEnd, 0, ObjectEnd.Length);
					break;
				case JsonToken.Null:
					this.WriteJson(Null, 0, Null.Length);
					break;
				default:
					throw new Exceptions.UnserializableJsonToken(token, this);
			}
		}
		public void Write(string value)
		{
			if (value == null)
				throw new ArgumentNullException("value");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			this.WriteFormatting(JsonToken.StringLiteral);

			var len = value.Length;
			var offset = 0;
			var outputOffset = 1;
			m_outputBuffer[0] = '"';
			while (offset < len)
			{
				var escaped = JsonUtils.EscapeBuffer(value, offset, m_outputBuffer, outputOffset, m_tmpBuffer);
				this.WriteJson(m_outputBuffer, 0, escaped + outputOffset);
				outputOffset = 0;
				offset += escaped;
			}
			m_outputBuffer[0] = '"';
			this.WriteJson(m_outputBuffer, 0, 1);
		}
		public void Write(JsonMember member)
		{
			this.WriteFormatting(JsonToken.Member);

			if (member.IsEscapedAndQuoted)
			{
				if (member.NameString != null)
					this.WriteJson(member.NameString);
				else
					this.WriteJson(member.NameChars, 0, member.NameChars.Length);
			}
			else
			{
				var memberName = member.ToString();
				var len = member.Length;
				var offset = 0;
				var outputOffset = 1;
				m_outputBuffer[0] = '"';
				while (offset < len)
				{
					var escaped = JsonUtils.EscapeBuffer(memberName, offset, m_outputBuffer, outputOffset, m_tmpBuffer);
					this.WriteJson(m_outputBuffer, 0, escaped + outputOffset);
					outputOffset = 0;
					offset += escaped;
				}
				m_outputBuffer[0] = '"';
				m_outputBuffer[1] = NameSeparator[0];
				this.WriteJson(m_outputBuffer, 0, 2);
			}			
		}
		public void Write(int number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.Int32ToBuffer(number, m_outputBuffer, 0, this.Context.Format);
			this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(uint number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.UInt32ToBuffer(number, m_outputBuffer, 0, this.Context.Format);
			this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(long number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.Int64ToBuffer(number, m_outputBuffer, 0, this.Context.Format);

			if (number > JsNumberMaxValueInt64)
				this.WriteString(new string(m_outputBuffer, 0, len));
			else
				this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(ulong number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.UInt64ToBuffer(number, m_outputBuffer, 0, this.Context.Format);

			if (number > JsNumberMaxValueUInt64)
				this.WriteString(new string(m_outputBuffer, 0, len));
			else
				this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(float number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.SingleToBuffer(number, m_outputBuffer, 0, this.Context.Format);
			if (number > JsNumberMaxValueSingle)
				this.WriteString(new string(m_outputBuffer, 0, len));
			else
				this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(double number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.DoubleToBuffer(number, m_outputBuffer, 0, this.Context.Format);
			if (number > JsNumberMaxValueDouble)
				this.WriteString(new string(m_outputBuffer, 0, len));
			else
				this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(decimal number)
		{
			this.WriteFormatting(JsonToken.Number);

			var len = JsonUtils.DecimalToBuffer(number, m_outputBuffer, 0, this.Context.Format);
			if (number > JsNumberMaxValueDecimal)
				this.WriteString(new string(m_outputBuffer, 0, len));
			else
				this.WriteJson(m_outputBuffer, 0, len);
		}
		public void Write(DateTime datetime)
		{
			this.WriteFormatting(JsonToken.DateTime);

			var dateString = datetime.ToString(this.Context.DateTimeFormats[0]);
			this.Write(dateString);
		}
		public void Write(bool value)
		{
			this.WriteFormatting(JsonToken.DateTime);

			if (value)
				this.WriteJson(True, 0, True.Length);
			else
				this.WriteJson(False, 0, False.Length);
		}

		public void Reset()
		{
			m_charsWriten = 0;
			m_struct.Clear();
		}

		private void WriteNewlineAndPad(int correction)
		{
			// add padings and linebreaks
			this.WriteJson(Newline, 0, Newline.Length);
			var tabs = m_struct.Count + correction;
			while (tabs > 0)
			{
				this.WriteJson(Tabs, 0, Math.Min(tabs, Tabs.Length));
				tabs -= Tabs.Length;
			}
		}
		private void WriteFormatting(JsonToken token)
		{
			if (m_struct.Count > 0)
			{
				var structPeek = m_struct.Peek();
				var isNotMemberValue = ((structPeek & Structure.IsObject) != Structure.IsObject || token == JsonToken.Member);
				var isEndToken = token == JsonToken.EndOfArray || token == JsonToken.EndOfObject;

				if ((structPeek & Structure.IsContainer) == Structure.IsContainer && isNotMemberValue)
				{
					// it's a begining of container we add padding and remove "is begining" flag
					if ((structPeek & Structure.IsBeginingOfContainer) == Structure.IsBeginingOfContainer)
					{
						structPeek = m_struct.Pop();
						m_struct.Push(structPeek ^ Structure.IsBegining); // revert "is begining"
					}
					// else if it's new array's value or new object's member put comman and padding
					else if (!isEndToken)
						this.WriteJson(ValueSeparator, 0, ValueSeparator.Length);

					// padding
#if DEBUG
					// pad only before member in object container(not before value, it's ugly)
					this.WriteNewlineAndPad(this.InitialPadding + (isEndToken ? -1 : 0));
#endif
				}
			}
		}
	}
}
